Skip to content

fix(host): stop storing the host's seq as a guest room's joined floor - #2613

Merged
Birfy merged 1 commit into
mainfrom
dev/2612-room-share-local-joined-floor
Nov 7, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev/2612-room-share-local-joined-floor

Conversation

@Birfy

@Birfy Birfy commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev
Fixes #2612
Owner: Dev
Reviewer: Reviewer

Why

A room created from an inbound join stored the envelope's seq — the host room's own line counter — as the guest's sharedFrom.joinedSeq. The guest numbers its mirrored log from 1 and linesVisibleTo keeps a line only when (line.seq ?? 0) >= from. With a join carrying seq: 202 and two history lines, the guest ended up with room.seq = 2, joinedSeq = 202, log = [1, 2]: roomHistory answered 0 lines, and a new message (local seq 3) stayed invisible too, so the room stayed blank until the host's counter reached 202. A non-numeric seq ("abc", {}, [1,2]) was stored as-is; the comparison against a string is always false, so that room never showed anything again.

What changed

core/src/host/room-membership.js — the room-creation branch of applyInboundRoomShare no longer writes joinedSeq: data.seq, so the existing draft.joinedSeq ?? 0 applies, exactly the way the re-seat path already behaves. The wire seq is the host's counter and is not a floor on this side. No clamp was added to linesVisibleTo: the filter is shared by every reader, and hiding the value there would keep the wrong state instead of fixing it.

core/test/room-membership.test.js — three #2612 cases in the existing file: a seq: 202 join carrying only the last two history lines shows both of them to the invited Bot; a line that arrives after the join is visible every time; and junk seq values ("abc", {}, [1,2], sent as raw envelope text because encodeRoomShare coerces seq) never land in state.json and never empty the room. All three fail on the unchanged source.

docs/subsystems/core-host.md — one invariant sentence in the room-share bullet: a mirror's membership floor is the guest's own log, never the envelope. (core-host.md owns core/src/host/.)

changelog.d/2612.fixed.md — the user-visible line.

patches/ds-bot/files/... — the two regenerated mirrors, byte-identical to core/ (sha256 compared; check then reports core/ equals ds-bot/ + patches/ds-bot/).

Ran at this tip:

$ cd core && DSH_HARNESS=/data/dsh/bot/workspace/d2607/repo/core/dev/harness TZ=UTC timeout 600 node --import ./test/harness-resolve.mjs --test test/room-membership.test.js
# tests 20
# pass 20
# fail 0

# same tip, room-membership.js:2523 reverted to main's `joinedSeq: data.seq`
$ cd core && DSH_HARNESS=/data/dsh/bot/workspace/d2607/repo/core/dev/harness TZ=UTC timeout 600 node --import ./test/harness-resolve.mjs --test test/room-membership.test.js
not ok 18 - #2612: a joined Bot sees the history the share carried, not the host seq as its floor
not ok 19 - #2612: a line that arrives after the join is visible in the joined Bot every time
not ok 20 - #2612: a junk wire seq never lands in state.json and never empties the room
# tests 20
# pass 17
# fail 3

$ cd core && DSH_HARNESS=/data/dsh/bot/workspace/d2607/repo/core/dev/harness TZ=UTC timeout 600 node --import ./test/harness-resolve.mjs --test test/remote-room.test.js test/room-ticket-seat.test.js
# tests 137
# pass 137
# fail 0

$ node scripts/ds-bot-patches.mjs check
skipped pin (no git): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/

$ (cd core && npx --no-install oxlint src test)
Found 81 warnings and 0 errors.  (all pre-existing; the touched files add none)

$ node .github/scripts/changelog.mjs check
changelog: 16 fragment(s) in changelog.d/ are well formed

$ node .github/scripts/check-mojibake.mjs --check
mojibake: ok — 0 occurrences of U+FFFD in 1540 text file(s)

Tip db5ea17b90d3c5d862db2b4cb3037bab55f3f8ac, built on the live main 836b83c — that commit plus these six files, byte for byte. The red run above is the same tip with only room-membership.js's removed line restored.

Not run, and why

  • npm run verify at the repo root — this box is shared with other lines and the task scopes verification to the target tests; two full suites would exhaust it.
  • node scripts/build-client.mjs --check — core/node_modules is not installed here (esbuild missing); no core/src/client/ file changed, so client.js is untouched.
  • core/scripts/check-arch-docs.mjs — needs git to diff against origin/main, unavailable here; the owned page is updated in this change, so the rule is satisfied and CI runs it.
  • The smoke test — not run, per the brief's target-tests-only instruction.

Notes

  • A room already stored with a bad floor (a guest whose state.json holds joinedSeq: 202) stays as it is until it is re-seated; there is no migration in this change, per Triage's out-of-scope line.
  • The host keeps sending seq on the wire for older guests; only the guest stops storing it as a floor.

Checkpoint

{
  "pr": 2613,
  "claim": "Dev",
  "issue": 2612,
  "summary": "A guest room created from a share no longer copies the envelope's seq into sharedFrom.joinedSeq, so the history the join carried and every later line are visible instead of being filtered out until the host's line counter caught up. A malformed seq can no longer become a stored floor that empties the room. Three tests cover a seq-202 join, a line after the join, and junk seq values; all three fail without the change. The subsystem page records that a mirror's membership floor is the guest's own log, never the envelope.",
  "files": [
    "core/src/host/room-membership.js",
    "core/test/room-membership.test.js",
    "docs/subsystems/core-host.md",
    "changelog.d/2612.fixed.md",
    "patches/ds-bot/files/src/host/room-membership.js",
    "patches/ds-bot/files/test/room-membership.test.js"
  ],
  "checks": {
    "ran": "cd core && node --import ./test/harness-resolve.mjs --test test/room-membership.test.js; node scripts/ds-bot-patches.mjs check; oxlint; changelog.mjs check; check-mojibake.mjs",
    "result": "green",
    "at": "db5ea17b90d3c5d862db2b4cb3037bab55f3f8ac"
  },
  "next": "none — ready for review"
}

applyInboundRoomShare copied the wire `seq` - the host room's own line
counter - into the guest's `sharedFrom.joinedSeq`, while the guest numbers
its mirrored log from 1. The history the join carried, and every line after
it, were filtered out until the host's counter caught up, and a malformed
seq left the room permanently empty.

Refs #2612
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 7, 2026
@Birfy Birfy self-assigned this Nov 7, 2026
@Birfy

Birfy commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at db5ea17
Written-by: Reviewer
Attestation: db5ea17 role=Reviewer

Reviewed tip db5ea17b90d3c5d862db2b4cb3037bab55f3f8ac from the API tarball of that sha, base 836b83c61bc700737c84a25faf9bad601fd2d70d, not from the author's narrative. Tier T3 (path core/src/host/room-membership.js, via risk-gate.mjs --pr 2613).

Scope and conformance

  • 6 files, exactly the six claimed; nothing else (no core/client.js, ruleset, CI config or other subsystem) in diff -rq base tip.
  • core/src/host/room-membership.js does one thing: the room-creation branch of applyInboundRoomShare no longer writes joinedSeq: data.seq (−1 line, +2 comment lines). joinedSeqOf / linesVisibleTo are untouched, so no clamp was moved into the shared filter — the decision written on room-share: a guest's joined floor is the host's seq, so a shared room shows nothing and never catches up #2612.
  • patches/ds-bot/files/src/host/room-membership.js and patches/ds-bot/files/test/room-membership.test.js are byte-identical to their core/ counterparts (cmp; the API also reports one blob sha per pair, 945274bf1178bfad2d6a5664b0bec0a968a239ed and 5d1ab5799909fa192807ae0c9fde88ddb66ee5ef).
  • docs/subsystems/core-host.md (owns core/src/host/) gains the invariant sentence; changelog.d/2612.fixed.md is well formed.
  • Body: Fixes #2612, Owner: Dev, Reviewer: Reviewer, label agent-pr, and a valid ## Checkpoint block (pr 2613, claim Dev, issue 2612, 524-char summary, 6 files, checks.at the full tip, next set). No hold, no comment asking not to merge.

Ran:

  • /data/dsh/bot/workspace/insp2613/tip/core DSH_HARNESS=/data/dsh/bot/workspace/d2607/repo/core/dev/harness TZ=UTC node --import ./test/harness-resolve.mjs --test test/room-membership.test.js → pass 20/20 (the three room-share: a guest's joined floor is the host's seq, so a shared room shows nothing and never catches up #2612 cases ok 18-20)
  • same tree with only core/src/host/room-membership.js replaced by the base blob (red-first) → pass 17, fail 3 — exactly not ok 18, 19, 20
  • same tree ... --test test/remote-room.test.js test/room-ticket-seat.test.js → pass 137/137
  • cmp of both patches/ds-bot/files/ mirrors against core/ → identical
  • PATH=/data/dsh/bot/workspace/tools/ghbin2:$PATH node .github/scripts/risk-gate.mjs --pr 2613 --repo ThinkFlowLab/FlyBot → risk-tier: T3
  • diff -rq of the base and tip tarballs → the six files and nothing else

Not run:

  • npm run verify at the repo root — shared box, and the brief scoped this verification to the target tests; tests-ok is green at the tip.
  • the smoke test — src/host/ change, but the brief scoped verification to the target tests; the changed branch is covered by the unit tests above.
  • node scripts/build-client.mjs --check — no core/src/client/ file changed, client.js untouched.

@github-actions github-actions Bot added the pass label Nov 7, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit a754cc4 Nov 7, 2026
18 of 19 checks passed
@Birfy
Birfy deleted the dev/2612-room-share-local-joined-floor branch November 7, 2026 14:04
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 7, 2026
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 10, 2026
…ws nothing, and never heals (ThinkFlowLab#2759)

Fixes ThinkFlowLab#2754

Owner: Dev-1
Written-by: Dev-1
Bot: Dev-1
Claim-Id: 020fcfd92ed647d0b83d1fce58a70ae6
Base: f2ef4df
Risk tier: T3

## What changed

A room whose stored join floor sits above its own `room.seq` shows no
lines at all, and every new line lands below the floor too, so the room
never heals: `joinedSeqOf` returned the stored value unchanged and both
read entries filtered by it. The floor comes from a host counter that
reached a participant record (or `sharedFrom`) before ThinkFlowLab#2612/ThinkFlowLab#2613
stopped writing it.

After: the computed floor is clamped on the read path. When it is above
the room's own `room.seq`, both `rt.linesVisibleTo` and `rt.roomHistory`
read from `room.seq` instead, so a room at `room.seq = 2` with a stored
floor of 202 shows the line at seq 2 (and every line that arrives
later), while a healthy floor is untouched. The clamped value is
memoised per room and reader at the `room.seq` of the first read —
recomputing it against the seq of every later read would slide the
window forward and hide the line the guest joined at — and that memo is
read before the "floor is healthy" early return: without that, the room
goes dark a second time from the other side, because once `room.seq`
reaches the stored floor the lookup stops clamping and answers with the
stored floor again, dropping every row the reader had already seen. A
reader that really rejoins (a new `joinedSeq`, or the same number joined
again later, a new `joinedAt`) gets a new interval, so its memo entry is
dropped rather than reused. One `warn` per room names the room id, the
pre-clamp floor and `room.seq`. The state object is never rewritten, so
no later `save()` can turn the clamp into a fact on disk.

The module-level `joinedSeqOf` / `linesVisibleTo` stay pure; only the
instance-level adapters pass through the clamp.

A room saved before `seq` existed has no counter to clamp against. Its
log's last seq is the number `ensureLog` would write on the next append,
so that is used instead of 0 — clamping to 0 would show the guest the
lines from before it joined, the over-read this fix must not create.

Issue shape note: as the issue records after its 2026-11-10T00:18:20Z
update, A (participants 202 / sharedFrom 202) and A2 (participants 202 /
sharedFrom 0) assert **1 row**; A3 (participants 0 / sharedFrom 202)
asserts **2 rows** because the participant's value wins, the clamp does
not fire, and that is the positive control that the priority rule was
not inverted. After mirroring a line at seq 3, A and A2 assert **2
rows**.

## Verification at the tip

Tip SHA: 8eead21
Tree: 9690e6b — the same tree as the
local verified commit `193036664dcea4df1961a29a9e74732d0e61731f`;
`.flybot_push.py --update` re-committed that tree on the live base
(`f2ef4dfacc`), so every run below was taken on this tree and this head
names the PR tip.

Ran:

Second round (`8eead216`), the memo read before the early return:

- red first on the previous tip `4033f570`, which carries the clamp but
not this fix, before any source change in this round:
`test/room-membership.test.js` `# tests 26 / # pass 25 / # fail 1` —
`not ok 13 - ThinkFlowLab#2754: a clamped floor keeps its window once room.seq
reaches the stored floor`, actual `['five']`, expected `['Bob joined',
'three', 'four', 'five']` at floor 5 / `room.seq` 5. The same test
covers floor 202 with the room grown to `seq` 300 (299 rows, first row
still `Bob joined`).
- after the fix, the same file: `# tests 26 / # pass 26 / # fail 0`,
including the rejoin case (new `joinedSeq` 6 at `joinedAt` 2 reads from
`['six']`, not the old window).

First round (this tip carries both changes), kept for the clamp itself:

- red first on a clean `f2ef4dfaccedd40315bd585474d0902118dc0ba8` tree
carrying only `core/test/room-membership.test.js` from this branch
(`core/src/host/room-membership.js` at the base): `# tests 25 / # pass
23 / # fail 2` — `not ok 12 - ThinkFlowLab#2754: a stored floor above room.seq is
clamped to room.seq on read` and `not ok 14 - ThinkFlowLab#2754: the clamp warns
once per room and never rewrites the stored floor`. The pure-helper case
(`0/1/2/3 -> 2/2/1/0`) passes at the base, as it must: the module-level
filter is unchanged.
- after the fix, the same file: `# tests 25 / # pass 25 / # fail 0`
- `TZ=UTC npm run verify` with
`ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8`, run on the
committed tree `193036664d`: exit 0 — core `# tests 3140 / # pass 3140 /
# fail 0` (the 26 room-membership cases are inside it), social 619, tree
140, timeline 161, workflow 508, `.github/scripts` + policy 671, interop
37, live-e2e 34
- `ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8 npm --prefix
core run arch-docs`, re-run on the committed tree: `arch-docs: OK (7
changed file(s))`
- `node scripts/ds-bot-patches.mjs generate` on this base wrote
identical bytes; `check` before and after the commit: `10 patches apply
cleanly in order`, `core/ equals ds-bot/ + patches/ds-bot/`; `canonical`
after the commit: `ok patches/ds-bot/ is what generate writes (generate
is a no-op)`
- `node .github/scripts/changelog.mjs check`: `17 fragment(s) in
changelog.d/ are well formed`

Not run: `dev/bench.sh` — no bench was started for this branch; the
change is a read-path filter in the host, covered by the room-membership
suite. No real-model end-to-end run: the room read surface is host-local
and the new cases drive it through the same `host()` fixture the file
already uses.

## Deviation from a plain source edit

`core/src/host/room-membership.js` and
`core/test/room-membership.test.js` are carried by
`patches/ds-bot/0012-room-membership.patch` through its
`patches/ds-bot/files/` mirror (that patch stores those files as a
mirror, not as patch-body hunks), so the branch carries both the source
pair and the mirrored pair; `generate` on this base reports the series
as what it writes.

## How an old reader sees it

No API shape, field name, or stored value changes. A guest of a healthy
room reads exactly the lines it read before, no `warn` fires for it, and
the saved `joinedSeq` / `sharedFrom` are untouched. Only a room whose
stored floor is above its own seq reads differently (from `room.seq`
instead of nothing), and only that room warns once. Once such a room's
own seq reaches the stored floor its window does not slide forward and
disappear: it keeps reading from the seq it clamped to, until the reader
rejoins.

## Checkpoint
<!-- flybot:checkpoint -->
```json
{
  "pr": 2759,
  "claim": "Dev-1",
  "issue": 2754,
  "summary": "A room whose stored join floor sits above its own room.seq rendered no lines and never healed, because every line and every new line fell below the floor. The read path clamps the computed floor to room.seq (to the log's last seq for a room saved before seq existed), for both rt.linesVisibleTo and rt.roomHistory, memoised per room and reader at the first read and warning once per room; the stored state is never rewritten, so no save turns the clamp into a fact on disk. The memo is read before the healthy-floor early return, so a room that grows past the stored floor keeps its window, and a rejoin drops it and reads from the new interval. Red first: 2 of 25 cases failed at f2ef4df, and 1 of 26 failed at 4033f570 for the growth case; the same file is 26/26 after the fix.",
  "files": [
    "core/src/host/room-membership.js",
    "core/test/room-membership.test.js",
    "docs/subsystems/team.md",
    "docs/subsystems/core-host.md",
    "changelog.d/2754-room-floor-clamp.fixed.md",
    "patches/ds-bot/files/src/host/room-membership.js",
    "patches/ds-bot/files/test/room-membership.test.js"
  ],
  "checks": {
    "ran": "TZ=UTC npm run verify with ARCH_DOCS_BASE=f2ef4dfa; core arch-docs; node scripts/ds-bot-patches.mjs generate/check/canonical; node .github/scripts/changelog.mjs check",
    "result": "green",
    "at": "8eead216f75d92e51c0e8b01c9bb541de3ed9e0d"
  },
  "next": "waiting for the review side's Verdict naming 8eead21"
}
```
<!-- /flybot:checkpoint -->
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 10, 2026
… seq reads a full room as empty (ThinkFlowLab#2791)

read_group_chat bypasses the join-floor clamp, so a Bot's floor above
seq reads a full room as empty

Fixes ThinkFlowLab#2790

Owner: Dev-1
Written-by: Dev-1
Bot: Dev-1
Reviewer: Birfy
Risk tier: T3
Base: 74d08c5
Claim-Id: 4bc854af8bdd4889a31174abc5395f83

## What changed

`read_group_chat` resolved its floor from the Bot's own participant
record and filtered `room.log` on the raw `joinedSeq`, so the ThinkFlowLab#2754
clamp — which the room reads of a human actor already run — never
applied to a Bot's read. A stored floor above the room's own `room.seq`
(the shape a host counter left on a participant record before
ThinkFlowLab#2612/ThinkFlowLab#2613 stopped the write) therefore hid every line from that reader
and answered `"<room>" has no messages yet` in a room the pane and every
other reader still showed.

The clamp body is now `clampedFloorOf(room, record, reader)` in
`core/src/host/room-membership.js`, keeping the memo, the one-time
warning and the no-`seq` fallback unchanged; the human reads resolve
their record and call it exactly as before, and the Bot read asks for
its own seat's floor through the new `rt.seatFloor(room, seat)`. The
memo key now carries the seat kind, so a human seat and a Bot seat that
happen to share an id cannot share a window. `core/src/host/groups.js`
uses it and keeps `botPart?.joinedSeq` as the fallback for a host where
the room-membership plugin is not installed.
`docs/subsystems/core-host.md` records that a Bot's own read takes the
same clamp, and one changelog fragment is added.

## Red first — clean base `74d08c5d`, only the new test on the branch

```text
$ node --import ./test/harness-resolve.mjs --test test/room-read-floor-clamp.test.js
not ok 1 - a Bot reads a room whose stored join floor is above room.seq
    a room with lines reads as empty: "Ops" has no messages yet.
not ok 2 - the clamped window opens onto lines that arrive afterwards
not ok 5 - a room without seq clamps to its stored log
# tests 5  # pass 2  # fail 3
```

The two guards that already pass are the healthy-floor read and an empty
room's own sentence, so the red is only the defect's face.

## After the fix

```text
$ node --import ./test/harness-resolve.mjs --test test/room-read-floor-clamp.test.js
# tests 5  # pass 5  # fail 0

$ node --import ./test/harness-resolve.mjs --test test/room-*.test.js test/group*.test.js test/face-*.test.js test/details-group-*.test.js test/mock-group-roster.test.js test/link-room-title.test.js
# 33 files: # tests 254  # pass 254  # fail 0

$ cd plugins/workflow && node --import ./test/resolve.mjs --test "test/*.test.js"
# tests 518  # pass 518  # fail 0
```

## Generated mirrors

`node scripts/ds-bot-patches.mjs generate` wrote `patches/ds-bot/files/
(391 files)` and all ten patch files. Against this base the change is
exactly three generated paths:

- `patches/ds-bot/0012-room-membership.patch` +6/−2 (the
`read_group_chat` hunk)
- `patches/ds-bot/files/src/host/room-membership.js` +10/−3
- `patches/ds-bot/files/test/room-read-floor-clamp.test.js` (new, 111
lines)

`check` answers all three lines ok — `ds-bot/ =
Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)`, `10 patches apply
cleanly in order`, `core/ equals ds-bot/ + patches/ds-bot/` — and
`canonical` reports `patches/ds-bot/ is what generate writes (generate
is a no-op)`.

## Ran

- `TZ=UTC npm run verify` — exit 0: core `# tests 3190 / # pass 3190`,
social 619, tree 143, timeline 161, workflow 518, `.github/scripts` +
policy 671, interop 37, live-e2e 34, zero failures.
- `node scripts/lint.mjs` — `Found 88 warnings and 0 errors` (the
warnings are on `main` too).
- `ARCH_DOCS_BASE=74d08c5d npm --prefix core run arch-docs` —
`arch-docs: OK (8 changed file(s))`.
- `node .github/scripts/changelog.mjs check` — `20 fragment(s) in
changelog.d/ are well formed`.
- `node scripts/ds-bot-patches.mjs generate` / `check` / `canonical` as
above.
- Verified commit `685c3210` (tree `5060cdd3`), eight files `+247/−10`;
the pushed head is generated from that tree by the push helper.

## Not run

- the smoke test: no `index.js` and no `src/host/` module wiring change
beyond the two functions above (the bench needs a harness checkout and a
port).
- `npm run pack`, typecheck and actionlint: not part of `npm run
verify`.

## Checkpoint

<!-- flybot:checkpoint -->
```json
{
  "pr": 2791, "claim": "Dev-1", "issue": 2790,
  "summary": "read_group_chat filtered the room log on the Bot's raw joinedSeq, so the ThinkFlowLab#2754 clamp the human room reads already run never applied: a stored floor above room.seq hid every line and answered has no messages yet in a room others still read. The clamp body is now clampedFloorOf(room, record, reader) and a Bot's read asks rt.seatFloor for its own seat, so the memo, the one-time warning and the no-seq fallback stay one rule. A new core/test/room-read-floor-clamp.test.js covers the clamp, the window healing, healthy floors, an empty room and a room saved before seq. generate, check and canonical are green, and the core and plugin suites pass.",
  "files": ["core/src/host/groups.js", "core/src/host/room-membership.js", "core/test/room-read-floor-clamp.test.js", "docs/subsystems/core-host.md", "changelog.d/2790-read-group-floor-clamp.fixed.md", "patches/ds-bot/0012-room-membership.patch", "patches/ds-bot/files/src/host/room-membership.js", "patches/ds-bot/files/test/room-read-floor-clamp.test.js"],
  "checks": { "ran": "TZ=UTC npm run verify (exit 0); ds-bot-patches generate/check/canonical; ARCH_DOCS_BASE=74d08c5d npm --prefix core run arch-docs", "result": "green", "at": "a2b157e744106de819841b883a81f93592532a52" },
  "next": "none — ready for review"
}
```
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

room-share: a guest's joined floor is the host's seq, so a shared room shows nothing and never catches up

1 participant